4. 컨테이너 실행

4.6 runc를 사용한 컨테이너 실행

runc란?

runc는 OCI Runtime Specification을 준수하도록 개발된 저수준 컨테이너 런타임. runc 바이너리의 서브 명령어로 기능을 구현하고, 상위 도구(고수준 런타임)에서는 runc를 서브 명령어와 함께 실행해서 컨테이너 생성, 시작, 정지, 삭제 등의 조작을 수행함

용어 정리

  • runc: OCI 표준을 구현한 참조 저수준 런타임. Go 언어로 작성되었으며, 원래 Docker의 libcontainer에서 분리되어 OCI에 기증됨
  • OCI (Open Container Initiative): 컨테이너 형식과 런타임에 대한 개방형 산업 표준 제정 단체
  • 바이너리(Binary): 컴퓨터가 직접 실행할 수 있는 기계어 코드 파일. 예: runc 실행 파일
  • 서브 명령어(Subcommand): 주 명령어 뒤에 붙는 하위 명령어. 예: runc run, runc create, runc start

컨테이너 실행 흐름:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[고수준 런타임] (containerd, CRI-O)
    │
    │ OCI Runtime Spec
    ↓
[runc] (저수준 런타임)
    │
    │ 시스템 콜
    ↓
[호스트 커널]
    ├─ Namespace (격리)
    ├─ cgroups (자원 제한)
    └─ seccomp (시스템 콜 필터)
    │
    ↓
[컨테이너 프로세스]

4.6.1 컨테이너 이미지를 가져오고 컨테이너 기반 작성

파일시스템 번들이란?

파일시스템 번들(Filesystem Bundle)은 runc가 컨테이너를 생성하는 데 필요한 모든 데이터를 저장하는 디렉터리. 다음 두 가지 요소로 구성됨

파일시스템 번들 구성요소:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[Filesystem Bundle]
    │
    ├─ config.json      ← 컨테이너 실행 환경 설정 파일
    │   │
    │   ├─ ociVersion   : OCI 버전 정보
    │   ├─ process      : 실행할 프로세스 정보
    │   │   ├─ args     : 실행 명령어와 인수
    │   │   ├─ env      : 환경 변수
    │   │   ├─ cwd      : 작업 디렉터리 (Current Working Directory)
    │   │   └─ user     : 실행 사용자 (uid/gid)
    │   │
    │   ├─ root         : 루트 파일시스템 경로
    │   ├─ hostname     : 컨테이너 호스트명
    │   │
    │   └─ linux        : Linux 전용 설정
    │       ├─ namespaces : 격리할 Namespace 목록
    │       ├─ resources  : cgroup 자원 제한
    │       └─ seccomp    : 시스템 콜 필터링 설정
    │
    └─ rootfs/          ← 컨테이너 루트 파일시스템
        ├─ bin/             (컨테이너 내부에서 /로 보임)
        ├─ etc/
        ├─ lib/
        ├─ usr/
        └─ ...
용어 정리

  • 루트 파일시스템(rootfs): 컨테이너가 /로 인식하는 최상위 디렉터리 구조. 호스트 파일시스템과 분리된 컨테이너 전용 파일시스템
  • config.json: 컨테이너 실행 환경 설정을 담은 JSON 형식 파일. OCI Runtime Spec에서 정의한 형식을 따름
  • Namespace(네임스페이스): 프로세스가 볼 수 있는 시스템 자원을 격리하는 Linux 커널 기능. pid(프로세스 ID), net(네트워크), mnt(마운트), uts(호스트명), ipc(프로세스 간 통신), user(사용자 ID) 등
  • cgroups(Control Groups): 프로세스의 자원(CPU, 메모리 등) 사용량을 제한하는 Linux 커널 기능

번들 디렉터리 생성:

# bundle 디렉터리 생성
# mkdir: Make Directory, 새 디렉터리를 생성하는 명령어
mkdir bundle

루트 파일시스템 준비:

컨테이너 루트 파일시스템을 가져와서 bundle 디렉터리에 저장. 실제 환경에서는 오버레이 파일시스템을 사용해서 이미지에 포함된 여러 레이어를 중첩하지만, 여기서는 docker export를 사용해 단순화된 방식으로 추출

용어 정리

  • 오버레이 파일시스템(Overlay Filesystem): 여러 레이어를 겹쳐서 하나의 파일시스템처럼 보이게 하는 기술. 컨테이너 이미지의 레이어를 효율적으로 병합
  • 레이어(Layer): 컨테이너 이미지를 구성하는 각각의 파일시스템 변경 단위. Dockerfile의 각 명령어가 새 레이어 생성

레이어 병합 예시:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[Layer 3] COPY app/ /app        → /app/* 추가
    ↓
[Layer 2] RUN apt install nginx → /usr/sbin/nginx 추가
    ↓
[Layer 1] FROM ubuntu           → 기본 OS 파일들

→ 최종 rootfs: 모든 레이어가 병합된 결과
# rootfs 디렉터리 생성
# bundle/rootfs: 컨테이너가 루트(/)로 인식할 파일시스템을 저장할 위치
mkdir bundle/rootfs

# Docker Hub에서 alpine 이미지 다운로드
# docker pull: 레지스트리에서 이미지를 로컬로 다운로드
# alpine:3.18: Alpine Linux 3.18 버전 (경량 Linux 배포판, 약 5MB)
docker pull alpine:3.18

# 임시 컨테이너 생성 및 백그라운드 실행
# docker run: 컨테이너 생성 및 실행
#   --rm: 컨테이너 종료 시 자동 삭제 (Remove)
#   --name tmp: 컨테이너 이름을 'tmp'로 지정
#   -d: Detached 모드, 백그라운드에서 실행
#   sleep infinity: 무한 대기 (컨테이너가 종료되지 않도록 유지)
docker run --rm --name tmp -d alpine:3.18 sleep infinity

# 컨테이너 파일시스템을 tar로 내보내고 rootfs에 추출
# docker export: 컨테이너의 파일시스템을 tar 아카이브로 내보냄
#   - 이미지가 아닌 실행 중인 컨테이너의 현재 상태를 추출
# | (파이프): 앞 명령어의 출력을 뒤 명령어의 입력으로 전달
# tar: 아카이브 처리 명령어
#   -x: Extract, 압축 해제
#   -C bundle/rootfs: Change directory, 추출할 디렉터리 지정
docker export tmp | tar -xC bundle/rootfs

# 추출된 파일 확인
# ls: List, 디렉터리 내용 표시
ls bundle/rootfs

실행 결과:

bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

이 시점에 bundle/rootfs 내부에 컨테이너 루트 파일시스템으로 사용할 파일들이 저장됨

실행 환경 설정 파일 생성:

# runc spec 명령어로 기본 설정 파일 생성
# runc spec: OCI Runtime Spec에 맞는 기본 config.json 생성
#   -b bundle: Bundle 디렉터리 경로 지정
#              생성된 config.json은 bundle/config.json에 저장됨
runc spec -b bundle

# 생성된 설정 파일 확인 (JSON 포맷팅)
# cat: Concatenate, 파일 내용 출력
# | jq: JSON 파서, 가독성 좋게 포맷팅 (jq가 없으면 cat만 실행)
cat bundle/config.json | jq

# 번들 디렉터리 구조 확인
# tree: 디렉터리 구조를 트리 형태로 표시
#   -L 1: Level 1, 1단계 깊이까지만 표시
tree -L 1 bundle

tree 실행 결과:

bundle
├── config.json      ← 컨테이너 실행 환경 설정 파일
└── rootfs           ← 컨테이너 루트 파일시스템 디렉터리

1 directory, 1 file

config.json 주요 내용:

{
  "ociVersion": "1.0.2-dev", // OCI Runtime Spec 버전
  "process": {
    // 컨테이너에서 실행할 프로세스 설정
    "terminal": true, // 터미널(TTY) 할당 여부
    "user": {
      // 프로세스 실행 사용자
      "uid": 0, // User ID (0 = root)
      "gid": 0 // Group ID (0 = root)
    },
    "args": ["sh"], // 실행할 명령어 (기본: sh 셸)
    "env": [
      // 환경 변수 목록
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "TERM=xterm" // 터미널 타입
    ],
    "cwd": "/" // Current Working Directory
  },
  "root": {
    // 루트 파일시스템 설정
    "path": "rootfs", // rootfs 디렉터리 경로
    "readonly": true // 읽기 전용 여부
  },
  "hostname": "runc", // 컨테이너 호스트명
  "linux": {
    // Linux 전용 설정
    "namespaces": [
      // 격리할 Namespace 목록
      { "type": "pid" }, // 프로세스 ID 격리
      { "type": "network" }, // 네트워크 격리
      { "type": "ipc" }, // 프로세스 간 통신 격리
      { "type": "uts" }, // 호스트명/도메인명 격리
      { "type": "mount" }, // 마운트 포인트 격리
      { "type": "cgroup" } // cgroups 격리
    ]
  }
}

4.6.2 컨테이너 실행

OCI 표준 조작과 runc 서브 명령어:

생성한 파일시스템 번들로 실제로 컨테이너를 실행. OCI Runtime Specification에 정의된 컨테이너 조작과 이에 대응하는 runc 서브 명령어

OCI 조작과 runc 서브 명령어:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[OCI 표준 조작]              [runc 서브 명령어]
─────────────────────────────────────────────

create (생성)        →       runc create
    │                        컨테이너 환경 준비
    │                        (프로세스는 아직 실행 안 함)
    ↓

start (시작)         →       runc start
    │                        준비된 컨테이너의 프로세스 시작
    ↓

kill (종료 신호)     →       runc kill
    │                        컨테이너 프로세스에 신호 전송
    ↓

delete (삭제)        →       runc delete
                             컨테이너 자원 정리 및 삭제

─────────────────────────────────────────────

run (실행)           →       runc run
                             create + start를 한 번에 수행
                             (편의 명령어)

컨테이너 실행 명령어:

고수준 런타임(containerd, CRI-O 등)이 runc를 조작할 때도 이런 서브 명령어를 사용. -b 옵션으로 파일시스템 번들을 지정하면 내부에 저장된 환경 정의 파일(config.json)과 루트 파일시스템(rootfs 디렉터리)을 바탕으로 컨테이너를 생성

# runc run 명령어로 컨테이너 생성 및 실행
# runc run: create와 start를 동시에 수행하는 편의 명령어
#   -b bundle: Bundle 디렉터리 경로 지정
#              bundle/config.json의 설정과 bundle/rootfs를 사용
#   myalpine: 컨테이너 이름 (ID)
#             동일한 이름으로 여러 컨테이너 생성 불가
runc run -b bundle myalpine

실행 결과:

/ #     ← 컨테이너 내부 셸 프롬프트
          '/'는 현재 위치가 루트 디렉터리
          '#'는 root 사용자임을 표시

컨테이너 내부 확인:

컨테이너 내부 셸에서 명령어를 실행하면 호스트와 격리된 환경임을 확인할 수 있음

# 컨테이너 내부에서 OS 정보 확인
# /etc/os-release: 운영체제 정보가 담긴 파일
cat /etc/os-release

실행 결과:

NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.18.0
PRETTY_NAME="Alpine Linux v3.18"
# 파일 목록 확인
# ls: 현재 디렉터리의 파일과 디렉터리 표시
ls

실행 결과:

bin    etc    lib    mnt    proc   run    srv    tmp    var
dev    home   media  opt    root   sbin   sys    usr

→ 호스트의 파일시스템이 아닌 컨테이너의 rootfs 내용이 표시됨

# 프로세스 목록 확인
# ps: Process Status, 실행 중인 프로세스 표시
#   a: 모든 사용자의 프로세스 표시
#   u: 사용자 친화적 형식으로 출력
#   x: 터미널에 연결되지 않은 프로세스도 표시
ps aux

실행 결과:

PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps aux

PID 1이 sh임을 주목. 컨테이너 내부에서는 sh가 첫 번째 프로세스(init). 호스트의 수많은 프로세스는 보이지 않음 (PID Namespace 격리)

호스트 vs 컨테이너 프로세스 격리:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[호스트 (Host)]              [컨테이너 (Container)]
─────────────────────────────────────────────────
PID 1: systemd               PID 1: sh
PID 2: kthreadd              PID 7: ps aux
PID 100: sshd
PID 200: dockerd             (호스트 프로세스가
PID 300: containerd           전혀 보이지 않음)
PID 500: nginx
...수백 개 프로세스

※ PID Namespace 격리로 컨테이너는 자신의 프로세스만 인식

4.6.3 컨테이너 정지와 삭제

OCI 표준의 kill과 delete 조작:

OCI Runtime Specification에 정의된 정지/삭제 조작도 runc의 서브 명령어로 구현

정지/삭제 조작:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[OCI 조작]    [runc 명령어]    [설명]
─────────────────────────────────────
kill          runc kill       컨테이너 프로세스에 신호(Signal) 전송
delete        runc delete     컨테이너 자원 정리 및 삭제

컨테이너 정지:

# 컨테이너에 KILL 신호 전송
# runc kill: 컨테이너 프로세스에 신호 전송
#   myalpine: 대상 컨테이너 이름
#   KILL: 전송할 신호
#         - KILL (SIGKILL, 9): 강제 종료, 무시 불가
#         - TERM (SIGTERM, 15): 정상 종료 요청, 프로세스가 무시 가능
#         - HUP (SIGHUP, 1): 재시작 요청
runc kill myalpine KILL

Unix/Linux 신호(Signal)란?

용어 정리

  • Signal(신호): 프로세스에게 특정 이벤트가 발생했음을 알리는 메커니즘. 프로세스 간 통신의 한 형태
  • SIGTERM(15): 정상 종료 요청 신호. 프로세스가 정리 작업 후 종료 가능하며 무시할 수 있음
  • SIGKILL(9): 강제 종료 신호. 즉시 프로세스 종료되며 무시 불가, 정리 작업 불가
  • SIGHUP(1): 연결 끊김/재시작 요청 신호. 설정 파일 재로드 시 자주 사용
  • SIGINT(2): 인터럽트 신호(Ctrl+C). 사용자가 실행 중단 요청
  • SIGSTOP(19): 프로세스 일시 정지 신호. 무시 불가
  • SIGCONT(18): 정지된 프로세스 재개 신호

컨테이너 삭제:

# 컨테이너 삭제
# runc delete: 컨테이너 자원 정리 및 삭제
#   myalpine: 삭제할 컨테이너 이름
#   ※ 컨테이너가 정지(stopped) 상태여야 삭제 가능
#   ※ 실행 중인 컨테이너를 강제 삭제하려면 --force 옵션 사용
runc delete myalpine

컨테이너 상태 확인:

# 컨테이너 목록 조회
# runc list: 현재 시스템의 모든 runc 컨테이너 표시
runc list

실행 결과 예시:

ID          PID         STATUS      BUNDLE              CREATED                          OWNER
myalpine    12345       running     /path/to/bundle     2024-01-15T10:30:00.123456789Z   root

4.6.4 컨테이너 생명주기 (Lifecycle)

컨테이너 생명주기 상태도:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    (없음)
       │
       │ runc create
       ↓
   ┌────────┐
   │creating│ → 컨테이너 환경 준비 중
   └───┬────┘
       │ (완료)
       ↓
   ┌────────┐
   │created │ → 컨테이너 생성됨 (아직 실행 안 됨)
   └───┬────┘
       │ runc start
       ↓
   ┌────────┐
   │running │ → 컨테이너 실행 중
   └───┬────┘
       │ (프로세스 종료 또는 runc kill)
       ↓
   ┌────────┐
   │stopped │ → 컨테이너 중지됨
   └───┬────┘
       │ runc delete
       ↓
    (삭제됨)

상태 설명:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
creating : 컨테이너 환경 준비 중
created  : 컨테이너 생성 완료, 프로세스 미실행
running  : 컨테이너 프로세스 실행 중
stopped  : 프로세스 종료됨, 자원 정리 대기

4.6.5 runc 주요 서브 명령어 요약

runc 서브 명령어:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[서브 명령어]    [설명]                              [사용 예]
────────────────────────────────────────────────────────────────
spec            기본 config.json 생성               runc spec -b bundle
create          컨테이너 생성 (프로세스 미실행)      runc create -b bundle mycontainer
start           생성된 컨테이너 시작                runc start mycontainer
run             생성과 시작을 동시에 수행           runc run -b bundle mycontainer
list            컨테이너 목록 조회                  runc list
state           컨테이너 상태 정보 (JSON)           runc state mycontainer
kill            컨테이너에 신호 전송                runc kill mycontainer SIGTERM
delete          컨테이너 삭제                       runc delete mycontainer
exec            실행 중인 컨테이너에서 추가 실행    runc exec mycontainer /bin/sh
pause           컨테이너 일시 정지                  runc pause mycontainer
resume          일시 정지된 컨테이너 재개           runc resume mycontainer

4.6.6 docker export vs docker save 비교

docker export vs docker save:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[docker export]
    │
    ├─ 대상: 컨테이너
    ├─ 출력: 단일 레이어 파일시스템
    ├─ 복원: docker import
    ├─ 용도: runc용 rootfs 추출, 백업
    └─ 히스토리: 손실

[docker save]
    │
    ├─ 대상: 이미지
    ├─ 출력: 모든 레이어 + 메타데이터
    ├─ 복원: docker load
    ├─ 용도: 이미지 배포, 백업
    └─ 히스토리: 보존
# docker export: 컨테이너 → tar (단일 레이어)
docker export container_name > container.tar

# docker save: 이미지 → tar (모든 레이어 + 메타데이터)
docker save image_name > image.tar

runc 컨테이너 실행 요약:

핵심 정리:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[1] 파일시스템 번들 준비
    ├─ rootfs 디렉터리: 컨테이너 루트 파일시스템
    └─ config.json: 컨테이너 실행 환경 설정

[2] 컨테이너 실행
    └─ runc run -b <bundle> <container-name>

[3] 컨테이너 관리
    ├─ runc list   : 컨테이너 목록 확인
    ├─ runc state  : 상태 상세 정보
    ├─ runc kill   : 정지 신호 전송
    └─ runc delete : 컨테이너 삭제

※ 고수준 런타임(containerd, CRI-O)은 이러한 runc 명령어를
   내부적으로 호출하여 컨테이너를 관리

참고 자료

공식 문서:

도구: